/*
 *
 *  Connection Manager
 *
 *  Copyright (C) 2007-2009  Intel Corporation. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <net/route.h>

#include <gdbus.h>

#include "connman.h"

#define	_DBG_CONNECTION(fmt, arg...)	DBG(DBG_CONNECTION, fmt, ## arg)

#define ACTIVE_METRIC 0
#define INACTIVE_METRIC 10
#define INVALID_METRIC -1

struct gateway_data {
	struct connman_element element;	/* CONNECTION element */
	char *gateway;			/* gateway address */
	unsigned int order;		/* sorted priority order */
	gboolean active;		/* TRUE if system told us installed */
	short metric;			/* metric used for this gateway */
};
static GSList *gateway_list = NULL;

static connman_bool_t checkorder(const struct gateway_data *data,
    const char *where)
{
	if (data->order == CONNMAN_SERVICE_ORDER_MAX) {
		connman_error("%s: bad order, gw %s index %d active %d",
		    where, data->gateway, data->element.index, data->active);
		return FALSE;
	} else
		return TRUE;
}

static struct gateway_data *find_gateway(int index, const char *gateway)
{
	GSList *list;

	if (gateway == NULL)		/* TODO(sleffler) needed? */
		return NULL;

	for (list = gateway_list; list; list = list->next) {
		struct gateway_data *data = list->data;

		if (data->element.index == index &&
		    g_strcmp0(data->gateway, gateway) == 0)
			return data;
	}
	return NULL;
}

static void update_order(void)
{
	GSList *list;

	for (list = gateway_list; list != NULL; list = list->next) {
		struct gateway_data *data = list->data;
		struct connman_service *service;

		service = __connman_element_get_service(&data->element);
		data->order = __connman_service_get_order(service);
		(void) checkorder(data, __func__);
	}
}

static void add_gateway(struct gateway_data *data)
{
	/* TODO(sleffler) check return value */
	gateway_list = g_slist_append(gateway_list, data);

	update_order();
}

static void connection_newroute(void *user_data, int index, int scope,
    const char *dst, const char *gateway)
{
	struct gateway_data *data;

	_DBG_CONNECTION("index %d scope %d dst %s gateway %s",
	    index, scope, dst, gateway);

	if (scope != RT_SCOPE_UNIVERSE || g_strcmp0(dst, "0.0.0.0") != 0)
		return;

	data = find_gateway(index, gateway);
	if (data != NULL)
		data->active = TRUE;
}

static void set_gateway_and_metric(struct gateway_data *data, short metric)
{
	struct connman_element *element = &data->element;
	short old_metric = data->metric;

	_DBG_CONNECTION("gateway %s metric %d", data->gateway, metric);

	/* TODO(sleffler) should we ignore this error? (do elsewhere) */
	data->metric = metric;
	if (connman_inet_set_gateway(element->index,
				     inet_addr(data->gateway),
				     data->metric) < 0) {
		/* NB: connman_inet_set_gateway logs a msg */
		return;
	}

	/* Remove the gateway with the old metric - errors ignored */
	if (old_metric != data->metric && old_metric != INVALID_METRIC)
		connman_inet_del_gateway(element->index,
					 inet_addr(data->gateway),
					 old_metric);
}

static void set_default_gateway(struct gateway_data *data)
{
	struct connman_element *element = &data->element;
	struct connman_service *service;

	set_gateway_and_metric(data, ACTIVE_METRIC);

	/*
	 * Mark associated service as the ``active'' one; this causes,
	 * for example, dns proxy requests to go through servers
	 * associated with the gateway.
	 */
	service = __connman_element_get_service(element);
	if (service != NULL) {
		/*
		 * TODO(sleffler): sometimes the service lookup fails
		 * because the corresponding network has no group;
		 * don't know why.
		 */
		__connman_service_indicate_active(service, TRUE);
	}
}

static struct gateway_data *pick_default_gateway(void)
{
	struct gateway_data *found = NULL;
	unsigned int order = CONNMAN_SERVICE_ORDER_MAX;
	GSList *list;

	for (list = gateway_list; list; list = list->next) {
		struct gateway_data *data = list->data;

		if (checkorder(data, __func__) == FALSE)
			continue;
		if (found == NULL || data->order < order) {
			found = data;
			order = data->order;
		}
	}
	return found;
}

static void notify_gateway_removed(struct gateway_data *data)
{
	struct connman_element *element;
	struct connman_service *service;

	element = &data->element;
	service = __connman_element_get_service(element);
	if (service != NULL)
		__connman_service_indicate_active(service, FALSE);
}

static void remove_gateway(struct gateway_data *data)
{
	_DBG_CONNECTION("gateway %s", data->gateway);

	if (data->active == TRUE) {
		/* known to be in the routing tables; remove it */
		connman_inet_del_gateway(data->element.index,
		    inet_addr(data->gateway),
		    data->metric);
		data->active = FALSE;
	}
	gateway_list = g_slist_remove(gateway_list, data);
	update_order();
	notify_gateway_removed(data);
}

static void connection_delroute(void *user_data, int index, int scope,
    const char *dst, const char *gateway)
{
	struct gateway_data *data;

	_DBG_CONNECTION("index %d scope %d dst %s gateway %s",
	    index, scope, dst, gateway);

	if (scope != RT_SCOPE_UNIVERSE || g_strcmp0(dst, "0.0.0.0") != 0)
		return;

	data = find_gateway(index, gateway);
	if (data != NULL) {
		data->active = FALSE;
		notify_gateway_removed(data);
	}

	data = pick_default_gateway();
	if (data != NULL) {
		connman_info("Replace default gateway with %s", data->gateway);
		set_default_gateway(data);
	}
}

static struct connman_rtnl connection_rtnl = {
	RTNL_DEFINE("connection", CONNMAN_RTNL_PRIORITY_DEFAULT),

	.newroute	= connection_newroute,
	.delroute	= connection_delroute,
};

static struct gateway_data *find_active_gateway(void)
{
	GSList *list;

	_DBG_CONNECTION("");

	for (list = gateway_list; list; list = list->next) {
		struct gateway_data *data = list->data;

		if (checkorder(data, __func__) == FALSE)
			continue;
		if (data->metric == ACTIVE_METRIC)
			return data;
	}

	return NULL;
}

static void gateway_destruct(struct connman_element *element)
{
	struct gateway_data *data = (struct gateway_data *) element;

	_DBG_CONNECTION("index %d gateway %s", element->index, data->gateway);

	g_free(data->gateway);
}

/*
 * Create a connection element and register it as a child of the parent.
 */
struct connman_element *__connman_connection_create(
	struct connman_ipconfig *ipconfig, struct connman_element *parent)
{
	struct gateway_data *data;
	struct connman_element *connection;

	data = g_try_new0(struct gateway_data, 1);
	if (data == NULL) {
		connman_error("%s: no memory for connection element", __func__);
		return NULL;
	}
	data->active = FALSE;
	data->metric = INVALID_METRIC;

	connection = &data->element;
	__connman_element_initialize(connection);
	connection->type    = CONNMAN_ELEMENT_TYPE_CONNECTION;
	connection->index   = connman_ipconfig_get_index(ipconfig);
	connection->devname = g_strdup(connman_ipconfig_get_ifname(ipconfig));
	connection->destruct = gateway_destruct;

	if (connman_element_register(connection, parent) < 0) {
		connman_element_unref(connection);
		return NULL;
	} else
		return connection;
}

static DBusConnection *connection;

static void emit_state_change(DBusConnection *conn, const char *state)
{
	DBusMessage *signal;

	connman_dbus_send_property_changed_variant(CONNMAN_MANAGER_PATH,
	    CONNMAN_MANAGER_INTERFACE, "State", DBUS_TYPE_STRING, &state);

	signal = dbus_message_new_signal(CONNMAN_MANAGER_PATH,
	    CONNMAN_MANAGER_INTERFACE, "StateChanged");
	if (signal != NULL) {
		DBusMessageIter entry;

		dbus_message_iter_init_append(signal, &entry);
		dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING,
		    &state);
		g_dbus_send_message(conn, signal);
	}
}

connman_bool_t __connman_connection_is_online(void)
{
	return (__connman_element_count(NULL,
	    CONNMAN_ELEMENT_TYPE_CONNECTION) > 0);
}

/*
 * Called when a CONNECTION element is registered.  We know the
 * element is installed as a direct child of an ipconfig element
 * (see connman_ipconfig_bind) so grab the parent and use it to
 * find gateway information.
 */
static int connection_probe(struct connman_element *element)
{
	struct connman_service *service = NULL;
	const char *gateway = NULL;
	struct gateway_data *active_gateway = NULL;
	struct gateway_data *new_gateway = NULL;

	_DBG_CONNECTION("element %p name %s parent %p index %d",
	    element, element->name, element->parent, element->index);

	if (element->parent == NULL)
		return -ENODEV;
#if 0
	/* TODO(sleffler) could check for all possible types */
	if (element->parent->type != CONNMAN_ELEMENT_TYPE_IPV4)
		return -ENODEV;
#endif
	gateway = __connman_ipconfig_get_gateway(element->index);
	_DBG_CONNECTION("gateway %s", gateway);

	/* locate the associated service and mark it READY+enabled */
	service = __connman_element_get_service(element);
	__connman_service_indicate_state(service, CONNMAN_SERVICE_STATE_READY);
	connman_element_set_enabled(element, TRUE);


	if (gateway == NULL)		/* NB: unlikely but possible */
		goto out;
	new_gateway = (struct gateway_data *) element;
	new_gateway->gateway = g_strdup(gateway);
	if (new_gateway->gateway == NULL) {
		connman_error("%s: no memory for gateway string %s",
		    __func__, gateway);
		goto out;
	}
	new_gateway->metric = INVALID_METRIC;

	/*
	 * Add the gateway and potentially update the system-wide default.
	 */
	active_gateway = find_active_gateway();
	add_gateway(new_gateway);

	if (active_gateway == NULL) {
		connman_info("No previous default gateway, use %s",
		    new_gateway->gateway);
		set_default_gateway(new_gateway);
	} else if (new_gateway->order < active_gateway->order) {
		connman_info("Change default gateway, %s (%d) metric %d"
			     " to %s (%d) metric %d",
		    active_gateway->gateway, active_gateway->order,
		    active_gateway->metric,
		    new_gateway->gateway, new_gateway->order,
		    new_gateway->metric);
		/* NB: add first so there's always a default route */
		set_gateway_and_metric(new_gateway, ACTIVE_METRIC);

		/* Update metric on old active gateway */
		set_gateway_and_metric(active_gateway, INACTIVE_METRIC);
	} else {
		connman_info("Adding inactive gateway for %s (%d) metric %d",
		    new_gateway->gateway, new_gateway->order,
		    INACTIVE_METRIC);
		set_gateway_and_metric(new_gateway, INACTIVE_METRIC);
	}

out:
	if (__connman_element_count(NULL, CONNMAN_ELEMENT_TYPE_CONNECTION) == 1)
		emit_state_change(connection, "online");

	return 0;
}

/*
 * Called when a CONNECTION element is removed from the hierarchy.
 */
static void connection_remove(struct connman_element *element)
{
	struct connman_service *service;
	struct gateway_data *data, *default_gateway;
	gboolean wasactive;

	_DBG_CONNECTION("element %p name %s", element, element->name);

	service = __connman_element_get_service(element);
	__connman_service_indicate_state(service,
	    CONNMAN_SERVICE_STATE_DISCONNECT);
	connman_element_set_enabled(element, FALSE);

	if (__connman_element_count(NULL, CONNMAN_ELEMENT_TYPE_CONNECTION) == 0)
		emit_state_change(connection, "offline");

	data = (struct gateway_data *) element;
	wasactive = data->active;	/* record before state is clobbered */
	remove_gateway(data);

	if (wasactive) {
		/* pick new active/default gateway as we just removed it */
		default_gateway = pick_default_gateway();
		if (default_gateway != NULL) {
			connman_info("New default gateway %s (%d) metric %d",
			    default_gateway->gateway,
			    default_gateway->order,
			    default_gateway->metric);
			set_default_gateway(default_gateway);
		}
	}
}

/*
 * Called when a CONNECTION element is updated (e.g. when roaming).
 */
static void connection_update(struct connman_element *element)
{
	const char *gateway = __connman_ipconfig_get_gateway(element->index);
	struct gateway_data *data = (struct gateway_data *) element;
	struct connman_service *service;

	_DBG_CONNECTION("element %p index %d gateway %s metric %d", element,
	     element->index, gateway, data->metric);

	if (gateway == NULL) {		/* NB: can happen during shutdown */
		connman_info("Update default gateway, %s to None",
			     data->gateway);
	} else if (g_strcmp0(data->gateway, gateway) != 0) {
		connman_info("Update default gateway, %s to %s",
		    data->gateway, gateway);

		/* NB: add first so there's always a default route */
		connman_inet_set_gateway(element->index,
		    inet_addr(gateway), data->metric);
		connman_inet_del_gateway(element->index,
		    inet_addr(data->gateway), data->metric);
		g_free(data->gateway);
		data->gateway = g_strdup(gateway);
	} else if (data->active == TRUE) {
		/* TODO(sleffler) should not be needed */
		connman_inet_set_gateway(element->index, inet_addr(gateway),
		    data->metric);
	}

	/* locate the associated service and mark it READY+enabled */
	service = __connman_element_get_service(element);
	__connman_service_indicate_state(service, CONNMAN_SERVICE_STATE_READY);
}

static struct connman_driver connection_driver = {
	.name		= "connection",
	.type		= CONNMAN_ELEMENT_TYPE_CONNECTION,
	.priority	= CONNMAN_DRIVER_PRIORITY_LOW,
	.probe		= connection_probe,
	.remove		= connection_remove,
	.update		= connection_update,
};


void __connman_connection_resort(void)
{
	struct gateway_data *new_gateway;
	struct gateway_data *active_gateway;

	active_gateway = find_active_gateway();
	update_order();
	new_gateway = pick_default_gateway();

	if (new_gateway != NULL && new_gateway != active_gateway) {
		/* NB: add first so there's always a default route */
		set_default_gateway(new_gateway);

		/* Update metric on old active gateway */
		if (active_gateway != NULL)
			set_gateway_and_metric(active_gateway, INACTIVE_METRIC);
	}
}


int __connman_connection_init(void)
{
	_DBG_CONNECTION("");

	connection = connman_dbus_get_connection();
	if (connection == NULL) {
		connman_error("No dbus connection");
		return -EIO;
	}
	if (connman_rtnl_register(&connection_rtnl) < 0) {
		connman_error("Failed to setup RTNL gateway driver");
		/* TODO(sleffler) continue? */
	}
	return connman_driver_register(&connection_driver);
}

void __connman_connection_cleanup(void)
{
	_DBG_CONNECTION("");

	connman_driver_unregister(&connection_driver);
	connman_rtnl_unregister(&connection_rtnl);

	dbus_connection_unref(connection);

	/* NB: storage is reclaimed with elements */
	while (gateway_list != NULL)
		remove_gateway(gateway_list->data);
}
